/*************************************************************************
 * Copyright 2008 David Almilli
 *  
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 *  
 *     http://www.apache.org/licenses/LICENSE-2.0 
 *      
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License.
 **************************************************************************/
package com.almilli.catnip.beacon;

import com.almilli.catnip.logging.Log;
import com.almilli.catnip.logging.LogFactory;
import com.almilli.catnip.web.HmeHost;
import com.tivo.hme.interfaces.IFactory;
import com.tivo.hme.interfaces.IHmeConstants;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.List;
import javax.jmdns.JmDNS;
import javax.jmdns.ServiceInfo;

/**
 * The <code>StandardTiVoBeacon</code> class is a simple implementation of TiVoBeacon that will
 * announce HME applications using JmDNS.
 * @author s2kdave
 */
public class StandardTiVoBeacon implements TiVoBeacon {

    private static final Log log = LogFactory.getLog(StandardTiVoBeacon.class);
    
    private String[] interfaces;
    private SafeJmDNS[] jmdns;
    private List<IFactory> factories = new ArrayList<IFactory>();
    private List<Integer> ports = new ArrayList<Integer>();
    private List<HmeHost> hosts = new ArrayList<HmeHost>();
    
    public StandardTiVoBeacon() {
        Thread shutdown = new Thread(new SafeShutdown(), "TiVoBeacon.Shutdown");
        Runtime.getRuntime().addShutdownHook(shutdown);
    }
    
    public boolean isStarted() {
        return (jmdns != null);
    }
    
    public void start() {
        if (jmdns != null) {
            throw new IllegalStateException("TiVo Beacon already started.");
        }
        log.info("Starting TiVo Beacon...");

        List<InetAddress> addrs = new ArrayList<InetAddress>();
        if (interfaces == null) {
            try {
                Enumeration<NetworkInterface> e = NetworkInterface.getNetworkInterfaces();
                while (e.hasMoreElements()) {
                    NetworkInterface ni = e.nextElement();
                    Enumeration<InetAddress> ea = ni.getInetAddresses();
                    while (ea.hasMoreElements()) {
                        InetAddress addr = ea.nextElement();
                        if (!addr.isLoopbackAddress()) {
                            addrs.add(addr);
                        }
                    }
                }
            } catch (SocketException se) {
                log.warn("Unable to detect interfaces.  You may have to set them manually.", se);
            }
        } else {
            for (String intf : interfaces) {
                try {
                    InetAddress addr = InetAddress.getByName(intf);
                    addrs.add(addr);
                } catch (UnknownHostException e) {
                    log.error("Cannot bind to interface: " + intf);
                }
            }
        }
        
        if (addrs.isEmpty()) {
            log.warn("No multicast interfaces to bind to.  The TiVo Beacon will not work until you fix it.");
        }
        
        jmdns = new SafeJmDNS[addrs.size()];
        for (int i=0; i < jmdns.length; i++) {
            InetAddress addr = addrs.get(i);
            try {
                if (log.isInfoEnabled()) {
                    log.info("TiVo Beacon interface: " + addr);
                }
                jmdns[i] = createJmDNS(addr);
            } catch (IOException e) {
                log.error("Cannot start TiVo Beacon on interface: " + addr, e);
            }
        }
    }
    
    protected SafeJmDNS createJmDNS(InetAddress addr) throws IOException {
        SafeJmDNS dns = new SafeJmDNS(addr);
        
        //remove the shutdown handler because it deadlocks and the beacon shuts down nicely
        try {
            Field field = JmDNS.class.getDeclaredField("shutdown");
            field.setAccessible(true);
            Thread oldShutdown = (Thread)field.get(dns);
            Runtime.getRuntime().removeShutdownHook(oldShutdown);
            field.set(dns, null);
            
        } catch (Exception e) {
            if (log.isDebugEnabled()) {
                //we still want a warning, just a stack trace too
                log.warn("Cannot override JmDNS shutdown handler.", e);
            } else {
                log.warn("Cannot override JmDNS shutdown handler.");
            }
        }
        
        return dns;
    }

    public synchronized void addHost(HmeHost host) {
        hosts.add(host);
        if (!isStarted()) {
            start();
        }
        for (int port : host.getPorts()) {
            if (ports.contains(port)) {
                return;
            }
            ports.add(port);
            
            //register all services on the new port
            for (IFactory factory : factories) {
                registerService(factory, port);
            }
        }
    }
    
    public synchronized void removeHost(HmeHost host) {
        hosts.remove(host);
        if (isStarted() && hosts.isEmpty()) {
            stop();
        } else {
            for (int port : host.getPorts()) {
                if (!ports.contains(port)) {
                    return;
                }
                if (ports.remove((Integer)port)) {
                    //unregister all services on the removed port
                    for (IFactory factory : factories) {
                        unregisterService(factory, port);
                    }
                }
            }
        }
    }
    
    public synchronized void stop() {
        if (jmdns != null) {
            log.info("Stopping TiVo Beacon...");

            //print log for unregister all services on all ports, the unregister is handled by close
            for (int port : ports) {
                for (IFactory factory : factories) {
                    if (log.isInfoEnabled()) {
                        log.info("Unregistering HME application " + factory.getAppTitle() + " on port " + port);
                    }
                }
            }
            factories.clear();
            ports.clear();
            
            for (int i=0; i < jmdns.length; i++) {
                jmdns[i].close();
            }
            jmdns = null;
        }
    }
    
	public synchronized void register(IFactory factory) {
        if (factories.contains(factory)) {
            return;
        }
        
        //register the new service on each port
        factories.add(factory);
        for (int port : ports) {
            registerService(factory, port);
        }
    }

    private void registerService(IFactory factory, int port) {
        if (log.isInfoEnabled()) {
            log.info("Registering HME application " + factory.getAppTitle() + " on port " + port);
        }
        
        for (JmDNS dns : jmdns) {
            try {
                dns.registerService(getServiceInfo(IHmeConstants.MDNS_TYPE, factory, port));
                
            } catch (IOException e) {
                if (log.isErrorEnabled()) {
                    log.error("Cannot register service on interface. " + factory, e);
                }
            }
        }
	}

	public synchronized void unregister(IFactory factory) {
        if (!factories.contains(factory)) {
            return;
        }
        
        //unregister the factory on each port
        if (factories.remove(factory)) {
	        for (int port : ports) {
	            unregisterService(factory, port);
	        }
        }
	}

    private void unregisterService(IFactory factory, int port) {
        if (log.isInfoEnabled()) {
            log.info("Unregistering HME application " + factory.getAppTitle() + " on port " + port);
        }

        for (SafeJmDNS dns : jmdns) {
            dns.unregisterService(getServiceInfo(IHmeConstants.MDNS_TYPE, factory, port));
        }
    }

    protected ServiceInfo getServiceInfo(String mdnsType, IFactory factory, int port) {
        Hashtable<String, String> args = new Hashtable<String, String>();
        args.put("path", factory.getAppName());
        args.put("version", (String)factory.getFactoryData().get(IFactory.HME_VERSION_TAG));
        return new ServiceInfo(mdnsType, factory.getAppTitle() + "." + mdnsType, port, 0, 0, args);
    }

    public String[] getInterfaces() {
        return interfaces;
    }

    public void setInterfaces(String[] interfaces) {
        this.interfaces = interfaces;
    }

    private class SafeShutdown implements Runnable {
        public void run() {
            stop();
        }
    }
    
    protected class SafeJmDNS extends JmDNS {

        public SafeJmDNS(InetAddress addr) throws IOException {
            super(addr);
        }
        
        
        public void close() {
            synchronized (StandardTiVoBeacon.this) {
                //we need to unregister all services first because we need to do it outside a sync
                //lock on the JmDNS instance or we get a deadlock
                unregisterAllServices();
    
                super.close();
            }
        }
    }
}
